﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Jb.Domain;
using Jb.Common.Extensions;
using Lucene.Net.Index;
using Lucene.Net.Store;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;

namespace Jb.Search.Indexer
{
    internal class JobIndexer : IIndexer
    {
        private IndexWriter _IndexWriter = null;
        private JbEntities _Context = null;

        #region Constructors

        public JobIndexer() 
        {            
            InitIndexWriter(SearchConstants.JobIndexDirectory);
        }
        
        #endregion Constructors

        #region Public IIndexer Members

        /// <summary>
        /// This method should use AddToLuceneIndex instead BuildLuceneDocument
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <param name="appName"></param>
        public void IndexDocument<T>(T obj, string appName) where T : class
        {
            IndexDocument(obj as Job, appName);
        }

        /// <summary>
        /// Deletes the existing index trace from Lucene and DB then Inserts index in Lucene and DB as well.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <param name="appName"></param>
        public void UpdateIndex<T>(T obj, string appName) where T : class
        {
            UpdateIndex(obj as Job, appName);
        }
        
        /// <summary>
        /// Deletes a Lucene Index by Job id, as we have configured lucene to think Job id as primary key.
        /// </summary>
        /// <param name="jobID"></param>
        public void DeleteIndex(int jobID)
        {
            _IndexWriter.DeleteDocuments(new Term("id", jobID.ToString()));
            JobIndex jobIndex = _Context.JobIndexes.FirstOrDefault(P => P.JobID == jobID);
            if (jobIndex != null)
            {
                _Context.DeleteObject(jobIndex);
                _Context.SaveChanges();                
            }
            //_IndexWriter.Commit();
        }

        public void Optimize()
        {
            _IndexWriter.Optimize();
        }
        public void CommitChanges()
        {
            _IndexWriter.Commit();
        }
        /// <summary>
        /// CloseWriter must be called after completing all indexing operation, to write the changes in disk.
        /// </summary>
        public void CloseWriter()
        {
            //_IndexWriter.Close();
            _IndexWriter.Dispose();
        }

        public void Rollback()
        {
            _IndexWriter.Rollback();
        }
        #endregion Public IIndexer Members

        #region Private Helper Methods

        /// <summary>
        /// Initializes Index Writer and Configures it index documents.
        /// </summary>
        /// <param name="indexDirectory"></param>
        private void InitIndexWriter(String indexDirectory)
        {
            if (String.IsNullOrEmpty(indexDirectory))
                throw new ArgumentException("Job Index directory cannot be null.");

            if (!System.IO.Directory.Exists(indexDirectory))
                System.IO.Directory.CreateDirectory(indexDirectory);

            Lucene.Net.Store.Directory directory = Lucene.Net.Store.FSDirectory.Open(indexDirectory);
            _IndexWriter = new IndexWriter(directory, 
                    new WhitespaceAnalyzer(),
                    //new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), 
                    IndexWriter.MaxFieldLength.UNLIMITED);

            _Context = new JbEntities();  
        }

        /// <summary>
        /// This method should use AddToLuceneIndex instead BuildLuceneDocument
        /// </summary>
        /// <param name="job"></param>
        /// <param name="appName">Name of the application which is creating this index</param>
        private void IndexDocument(Job job, String appName)
        {
            StringBuilder dbDocument = new StringBuilder();
            Document doc = BuildLuceneDocument(job, ref dbDocument);
            _IndexWriter.AddDocument(doc);
            JobIndex jobIndex = _Context.JobIndexes.FirstOrDefault(P => P.JobID == job.ID);
            if (jobIndex == null)
            {
                jobIndex = new JobIndex();
                _Context.JobIndexes.AddObject(jobIndex);
                //_IndexWriter.Commit();
                //_IndexWriter.Close();
            }
            SetPropertiesOfDbDocument(jobIndex, job, dbDocument, appName);
            _Context.SaveChanges();
        }

        /// <summary>
        /// Deletes the existing index trace from Lucene and DB then Inserts index in Lucene and DB as well.
        /// </summary>
        /// <param name="product"></param>
        private void UpdateIndex(Job job, String appName)
        {
            DeleteIndex(job.ID);
            IndexDocument(job, appName);
        }

        #endregion Private Methods

        #region Re-Index Products
        
        /// <summary>
        /// Deletes existing indexes and rebuilds index in DB and Lucene
        /// </summary>
        /// <returns></returns>
        public ReIndexResult ReIndex(int processStatusJobID)
        {
            ReIndexResult result = new ReIndexResult();
            Domain.ProcessStatus processStatus = _Context.ProcessStatuses.FirstOrDefault(P => P.ID == processStatusJobID);

            ///Delete All ProductIndexes from DB and Lucene Index
            foreach (JobIndex jobIndex in _Context.JobIndexes)
            {
                _IndexWriter.DeleteDocuments(new Term("id", jobIndex.JobID.ToString()));
                _Context.JobIndexes.DeleteObject(jobIndex);
                result.DeletedIndexFromDB = result.DeletedIndexFromLucene = result.DeletedIndexFromLucene + 1;
            }
            this.CommitChanges();
            _Context.SaveChanges();

            ///Now Re-Index all the active jobs
            var jobs = (from P in _Context.Jobs.Include("Category").Include("Company")
                       where P.Company.IsActive
                           && P.IsActive
                           && !P.IsDeleted
                       select P).ToList();
            result.FoundItems = jobs.Count();

            StringBuilder dbDocument = new StringBuilder();
            foreach (Job job in jobs)
            {
                dbDocument.Length = 0;
                //Prepare Lucene Document                
                Document luceneDocument = BuildLuceneDocument(job, ref dbDocument);
                ///Already deleted all Lucene index documents, so no need to delete them again
                ///Add new Document to Lucene Index
                _IndexWriter.AddDocument(luceneDocument);                
                //Update Index Document of DB with latest changes
                JobIndex jobIndex = _Context.JobIndexes.FirstOrDefault(P => P.JobID == job.ID);
                if (jobIndex == null)
                {
                    jobIndex = new JobIndex();
                    _Context.JobIndexes.AddObject(jobIndex);
                }
                SetPropertiesOfDbDocument(jobIndex, job, dbDocument, "JobIndexer.ReIndex");
                result.ReIndexedItems += 1;
                //Save new Document to DB and Update Progress
                processStatus.Completed += 1;
                _Context.SaveChanges();
            }
            processStatus.Status = QueueStatus.DONE;
            _Context.SaveChanges();
            this.CommitChanges();
            this.Optimize();
            this.CloseWriter();
            return result;
        }
        /// <summary>
        /// Builds a Lucene document for indexing 
        /// along with, creates a similar document to save in the database as well.
        /// </summary>
        /// <param name="job">The Job object to be indexed</param>
        /// <param name="sb">The reference of the DB document. Which will be updated with new document properties</param>
        /// <returns>Returns a Lucene Document</returns>
        private Document BuildLuceneDocument(Job job, ref StringBuilder sb)
        {            
            Document doc = new Document();
            String category = BuildCategoryPath(job);
            String location = String.Format("{0} - {1} - {2}", job.City.Name.ToUpper(), job.City.State.Name.ToUpper(), job.City.State.Country.Name.ToUpper());
            //String price = job.Price.ToString(SearchConstants.LUCENE_DECIMAL_FORMAT);
            doc.Add(new Field("id", job.ID.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("category", category.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("title", job.Title.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            //doc.Add(new Field("title_orig", job.Name, Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("description", job.Description.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            //if(!job.Model.IsNullOrEmpty())
            //    doc.Add(new Field("model", job.Model.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("companyid", job.CompanyID.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            doc.Add(new Field("company", job.Company.Name.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));
            doc.Add(new Field("location", location, Field.Store.NO, Field.Index.ANALYZED));

            NumericField salaryFromField = new NumericField("salary_from", Field.Store.YES, true);
            salaryFromField.SetDoubleValue((double)job.SalaryRangeFrom.GetValueOrDefault());
            doc.Add(salaryFromField);

            NumericField salaryToField = new NumericField("salary_to", Field.Store.YES, true);
            salaryToField.SetDoubleValue((double)job.SalaryRangeTo.GetValueOrDefault());
            doc.Add(salaryToField);

            doc.Add(new Field("created", 
                    DateTools.DateToString(job.CreatedOn, DateTools.Resolution.SECOND), 
                    Field.Store.YES, 
                    Field.Index.NOT_ANALYZED));
            doc.Add(new Field("modified",
                    DateTools.DateToString(job.ModifiedOn, DateTools.Resolution.SECOND),
                    Field.Store.YES,
                    Field.Index.NOT_ANALYZED));

            String requiredSkills = BuildJobSkills(job);

            doc.Add(new Field("full_content", String.Format("{0} {1} {2} {3} {4} {5}", category.ToUpper(), job.Company.Name.ToUpper(), job.Title.ToUpper(), job.Description.ToUpper(), requiredSkills, location)
                , Field.Store.NO, Field.Index.ANALYZED));

            ///Document to save in DB
            sb.AppendFormat("id: {0}{1}", job.ID, Environment.NewLine);
            sb.AppendFormat("category: {0}{1}", category.ToUpper(), Environment.NewLine);
            sb.AppendFormat("title: {0}{1}", job.Title.RemoveNewline().ToUpper(), Environment.NewLine);
            //sb.AppendFormat("title_orig:{0}{1}", job.Name, Environment.NewLine);
            sb.AppendFormat("description: {0}{1}", job.Description.RemoveNewline().ToUpper(), Environment.NewLine);
            
            sb.AppendFormat("companyid: {0}{1}", job.CompanyID, Environment.NewLine);
            sb.AppendFormat("company: {0}{1}", job.Company.Name.RemoveNewline().ToUpper(), Environment.NewLine);
            sb.AppendFormat("location: {0}{1}", location, Environment.NewLine);
            //sb.AppendFormat("price:{0}{1}", price, Environment.NewLine);
            sb.AppendFormat("salary_from: {0}{1}", ((double)job.SalaryRangeFrom.GetValueOrDefault()).ToString(), Environment.NewLine);
            sb.AppendFormat("salary_to: {0}{1}", ((double)job.SalaryRangeTo.GetValueOrDefault()).ToString(), Environment.NewLine);
            sb.AppendFormat("created: {0}{1}", DateTools.DateToString(job.CreatedOn, DateTools.Resolution.SECOND), Environment.NewLine);
            sb.AppendFormat("modified: {0}{1}", DateTools.DateToString(job.ModifiedOn, DateTools.Resolution.SECOND), Environment.NewLine);
            sb.AppendFormat("full_content: {0}{1}", String.Format("{0} {1} {2} {3} {4} {5}", category.ToUpper(), job.Company.Name.ToUpper(), job.Title.ToUpper(), job.Description.ToUpper(), requiredSkills, location), Environment.NewLine);

            ///Parse Dynamic XML Data
            if (!job.DynamicProperties.IsNullOrEmpty())
            {
                XDocument xmlDoc = XDocument.Parse(job.DynamicProperties);
                var properties = xmlDoc.Root.Elements("Property");
                foreach (var property in properties)
                {
                    String fieldName = GetAttributeValue(property, "Name"); //property.Attribute("Name").Value;
                    String fieldValue = GetAttributeValue(property, "Value"); //property.Attribute("Value").Value;
                    if (!fieldName.IsNullOrEmpty() && !fieldValue.IsNullOrEmpty())
                    {
                        if (fieldValue.IsNumeric())
                        {
                            NumericField numField = new NumericField(fieldName.ToLower(), Field.Store.YES, true);
                            numField.SetDoubleValue(fieldValue.ToDouble());
                            doc.Add(numField);
                            //doc.Add(new Field(fieldName, fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED));
                        }
                        else
                            doc.Add(new Field(fieldName.ToLower(), fieldValue.ToUpper(), Field.Store.NO, Field.Index.ANALYZED));

                        sb.AppendFormat("{0}: {1}{2}", fieldName.ToLower(), fieldValue.RemoveNewline().ToUpper(), Environment.NewLine);
                    }
                }
            }
            return doc;
        }        

        private string GetAttributeValue(XElement property, string attributeName)
        {
            if (property != null)
            {
                var attribute = property.Attribute(attributeName);
                if (attribute != null)
                    return attribute.Value;
            }
            return String.Empty;
        }

        /// <summary>
        /// Builds JobSkill Text for indexing
        /// </summary>
        /// <param name="job"></param>
        /// <returns></returns>
        private String BuildJobSkills(Job job)
        {
            StringBuilder sb = new StringBuilder(10);
            foreach (var jobSkill in job.JobSkills)
            {
                if (jobSkill.Skill == null)
                    sb.AppendFormat(" {0} ", jobSkill.OtherSkill.ToUpper());
                else
                    sb.AppendFormat(" {0} ", jobSkill.Skill.Name.ToUpper());
            }
            return sb.ToString();
        }

        /// <summary>
        /// Sets properties of the productIndex object to save in the databasse
        /// </summary>
        /// <param name="jobIndex"></param>
        /// <param name="job"></param>
        /// <param name="dbDocument"></param>
        /// <param name="appName"></param>
        private void SetPropertiesOfDbDocument(JobIndex jobIndex, Job job, StringBuilder dbDocument, String appName)
        {
            jobIndex.JobID = job.ID;            
            jobIndex.IndexedContent = dbDocument.ToString();
            jobIndex.ModifiedOn = DateTime.Now;
            jobIndex.HasInLucene = true;
            jobIndex.AppName = appName;
        }

        /// <summary>
        /// Builds a category path for a given job object.
        /// TODO: This should be recursive function. Currently it assumes the sub-category depth is 2
        /// </summary>
        /// <param name="job"></param>
        /// <returns>Returns a category string of the format /{parent}/{child1}/{child2}</returns>
        private string BuildCategoryPath(Job job)
        {
            StringBuilder sb = new StringBuilder(5);
            sb.AppendFormat("/ {0} ", job.Category.Name);
            if (job.Category.ParentCategory != null)
            {
                sb.AppendFormat("/ {0} ", job.Category.ParentCategory.Name);                
            }
            return sb.ToString();
        }
        #endregion
        
    }
}
